﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Security.Cryptography;
using System.Text.RegularExpressions;

namespace ggprot
{
    public class GaduProtocol : IDisposable
    {
        public const int LISTENING_DELAY = 128;
        public const int PING_DELAY = 180;

        public event EventHandler Connected;
        public event EventHandler Disconnected;
        public event AddressObtainedEventHandler AddressObtained;
        public event PacketEventHandler PacketReceived;
        public event LoginResponseReceivedEventHandler LoginResponseReceived;
        public event MessageReceivedEventHandler MessageReceived;
        public event TypingNotifyEventHandler TypingNotifyReceived;
        public event MultiLoginEventHandler MultiLogin;

        private bool connected;
        private int uin;
        private int seed;
        private int port;
        private IPAddress serverAddress;
        private PacketFactory factory;
        private TcpClient socket;
        private NetworkStream connectionStream;
        private Thread listenerThread;
        private DateTime lastPing;
        private List<User> users;
        private GGMultiloginItem[] parallelUsers;

        private string description;
        private Status status;

        private string fileStorage;

        public string FileStorage
        {
            get { return fileStorage; }
            set { fileStorage = value; }
        }

        public GGMultiloginItem[] ParallelUsers
        {
            get { return parallelUsers; }
        }

        public string Description
        {
            get { return description; }
            set { description = value; }
        }

        public Status Status
        {
            get { return status; }
            set { status = value; }
        }

        public List<User> Users
        {
            get { return users; }
            set { users = value; }
        }

        public NetworkStream ConnectionStream
        {
            get { return connectionStream; }
            set { connectionStream = value; }
        }

        public int Seed
        {
            get { return seed; }
            set { seed = value; }
        }

        public int Port
        {
            get { return port; }
            set { port = value; }
        }

        public IPAddress ServerAddress
        {
            get { return serverAddress; }
            set { serverAddress = value; }
        }

        public int Uin
        {
            get { return uin; }
            set { uin = value; }
        }

        public bool IsConnected
        {
            get { return connected; }
            set { connected = value; }
        }

        public void Dispose()
        {
            socket.Close();
            listenerThread.Abort();
        }

        public GaduProtocol()
        {
            lastPing = DateTime.Now;

            listenerThread = new Thread(new ThreadStart(Listen));

            users = new List<User>();

            fileStorage = @"\temp";
        }

        public void AddUser(User user)
        {
            users.Add(user);

            GGNotify notif = new GGNotify();
            notif.Type = (int)Packets.NotifyAdd;
            notif.Length = 5;
            notif.Uin = new int[] { user.Uin };
            notif.UsrType = new byte[] { (byte)user.UserType };

            factory.SendPacket(notif);
        }

        public void RemoveUser(User user)
        {
            users.Remove(user);

            GGNotify notif = new GGNotify();
            notif.Type = (int)Packets.NotifyRemove;
            notif.Length = 5;
            notif.Uin = new int[] { user.Uin };
            notif.UsrType = new byte[] { (byte)user.UserType };

            factory.SendPacket(notif);
        }

        public static byte[] CalculatePasswordSHA1(string password, int seed)
        {
            SHA1 sha = SHA1.Create();
            byte[] hash = new byte[64];
            byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
            byte[] seedBytes = BitConverter.GetBytes(seed);
            byte[] toEncode = passwordBytes.Concat(seedBytes).ToArray();
            sha.ComputeHash(toEncode).CopyTo(hash, 0);
            return hash;
        }

        public void Listen()
        {
            while (true)
            {
                if ((DateTime.Now - lastPing).TotalSeconds >= PING_DELAY)
                {
                    factory.SendPacket(new GGHeader(Packets.Ping, 0));
                    lastPing = DateTime.Now;
                }
                if (socket.Available < 8)
                {
                    Thread.Sleep(LISTENING_DELAY);
                    if (!socket.Connected)
                    {
                        if (Disconnected != null)
                            Disconnected(this, EventArgs.Empty);
                        break;
                    }
                    continue;
                }
                onPacketReceived(factory.GetPacket());
            }
        }

        private void onPacketReceived(GGHeader packet)
        {
            Console.WriteLine("Packet received: {0}", packet.Type);

            if (PacketReceived != null)
                PacketReceived(this, new PacketEventArgs(packet));

            switch (packet.Type)
            {
                case (int)Packets.Welcome:
                    seed = (packet as GGWelcome).Seed;
                    if (Connected != null)
                        Connected(this, EventArgs.Empty);
                    break;
                case (int)Packets.LoginOk:
                    if (LoginResponseReceived != null)
                        LoginResponseReceived(this, new LoginResponseReceivedEventArgs(true));
                    SendList();
                    break;
                case (int)Packets.LoginFailed:
                    if (LoginResponseReceived != null)
                        LoginResponseReceived(this, new LoginResponseReceivedEventArgs(false));
                    break;
                case (int)Packets.UserData:
                    GGUserData udata = (GGUserData)packet;
                    foreach (GGUserDataUser udataUsr in udata.Users)
                    {
                        User usr = users.Where(t => t.Uin == udataUsr.Uin).FirstOrDefault();
                        if (usr != null)
                            usr.Attribs = udataUsr.Attribs;
                    }
                    break;
                case (int)Packets.RecvMsg:
                    GGRecvMsg recv = (GGRecvMsg)packet;

                    GGRecvMsgAck recvAck = new GGRecvMsgAck();
                    recvAck.Type = (int)Packets.RecvMsgAck;
                    recvAck.Seq = recv.Seq;
                    recvAck.Length = 4;

                    factory.SendPacket(recvAck);

                    //Sprawdzanie, czy wiadomość zawiera obrazek
                    if (Regex.IsMatch(recv.Message, @"<img name=""[a-zA-Z0-9]+"">"))
                    {
                        Regex r = new Regex(@"<img name=""([a-zA-Z0-9]+)"">");
                        string imgName = r.Match(recv.Message).Groups[1].Value;

                        recv.Message = recv.Message.Replace(string.Format("<img name=\"{0}\">", imgName), string.Format("<img name=\"{0}\" src=\"{0}\" />", imgName));

                        if (!File.Exists(string.Format(@"{0}\{1}", fileStorage, imgName)))
                            requestImage(imgName, recv.Sender);
                    }

                    if (recv.Attrib != null)
                    {
                        if (recv.Attrib[0] == (byte)MessageAttribs.ImageReplyFirst || recv.Attrib[0] == (byte)MessageAttribs.ImageReply)
                        {
                            ImgReply img = new ImgReply();
                            img.ReadBytes(recv.Attrib);
                        }
                    }

                    if (MessageReceived != null)
                        MessageReceived(this, new MessageReceivedEventArgs(recv.Sender, recv.Message));
                    break;
                case (int)Packets.OwnMsg: goto case (int)Packets.RecvMsg;
                case (int)Packets.TypeNotif:
                    if (TypingNotifyReceived != null)
                    {
                        GGTypingNotify notif = (GGTypingNotify)packet;
                        TypingNotifyReceived(this, new TypingNotifyEventArgs(notif.Uin, notif.NotifType == 0 ? false : true));
                    }
                    break;
                case (int)Packets.MultiLoginInfo:
                    GGMultiloginInfo inf = (GGMultiloginInfo)packet;
                    parallelUsers = inf.Items;
                    if (MultiLogin != null)
                        MultiLogin(this, new MultiLoginEventArgs(inf));
                    break;
                case (int)Packets.Disconnected:
                    if (Disconnected != null)
                        Disconnected(this, EventArgs.Empty);
                    break;
            }
        }

        public void ObtainServer(int uin)
        {
            Uri server = new Uri(string.Format("http://appmsg.gadu-gadu.pl/appsvc/appmsg_ver8.asp?fmnumber={0}&version=10.1.0.11070", uin));
            using (WebClient client = new WebClient())
            {
                string respMessage = client.DownloadString(server);
                string[] respString = respMessage.Split('\n')[0].Split(' ');

                this.uin = uin;
                serverAddress = IPAddress.Parse(respString[2].Split(':')[0]);
                port = int.Parse(respString[2].Split(':')[1]);
                if (AddressObtained != null)
                    AddressObtained(this, new AddressObtainedEventArgs(true, string.Empty));
            }
        }

        public void DisconnectParallel(long id)
        {
            if (parallelUsers.Where(t => t.ConnId == id).Count() > 0)
            {
                GGMultiloginDisconnect mld = new GGMultiloginDisconnect();
                mld.Type = (int)Packets.MultiloginDisconnect;
                mld.Length = 8;
                mld.ConnID = id;

                factory.SendPacket(mld);
            }
        }

        public void Connect(IPAddress address, int port)
        {
            socket = new TcpClient(address.ToString(), port);
            connectionStream = socket.GetStream();
            listenerThread.Start();

            factory = new PacketFactory(connectionStream);
        }

        public void Login(int uin, string passwd, Status status = ggprot.Status.Avail, string description = null)
        {
            GGLogin login = new GGLogin();

            login.Type = (int)Packets.Login;

            login.Uin = uin;
            login.Language = "pl";
            login.HashType = 0x2;
            login.Hash = CalculatePasswordSHA1(passwd, seed);
            login.Status = (int)status;
            login.Flags = 0x1;
            login.Features = (int)(Features.MsgType | Features.NotifyType | Features.StatusType | Features.DndFfc | Features.LoginFailedType | Features.AddInfo | Features.SendMsgAck | Features.TypingNotif | Features.MultiLogin);
            login.LocalIp = 0;
            login.LocalPort = 0;
            login.ExternalIp = 0;
            login.ExternalPort = 0;
            login.ImageSize = 0xFF;
            login.Unknown2 = 0x64;
            login.Version = "Gadu-Gadu Client build 10.1.0.11070";
            login.VersionLen = login.Version.Length;
            login.Description = description;
            login.DescriptionSize = login.Description == null ? 0 : login.Description.Length;
            login.Length = 105 + login.VersionLen + login.DescriptionSize;

            this.Status = status;
            this.description = description;

            factory.SendPacket(login);
        }

        public void TypingNotify(int uin, bool typing)
        {
            GGTypingNotify notif = new GGTypingNotify();
            notif.Type = (int)Packets.TypeNotif;

            notif.NotifType = (short)(typing ? 1 : 0);
            notif.Uin = uin;

            notif.Length = 6;

            factory.SendPacket(notif);
        }

        public Status TranslateStatus(Status status, bool hasDescription)
        {
            switch (status)
            {
                case Status.Avail: return hasDescription ? Status.AvailDesc : Status.Avail;
                case Status.Busy: return hasDescription ? Status.BusyDesc : Status.Busy;
                case Status.DoNotDisturb: return hasDescription ? Status.DoNotDisturbDesc : Status.DoNotDisturb;
                case Status.FreeForChat: return hasDescription ? Status.FreeForChatDesc : Status.FreeForChat;
                case Status.Invis: return hasDescription ? Status.InvisDesc : Status.Invis;
                case Status.NotAvail: return hasDescription ? Status.NotAvailDesc : Status.NotAvail;
                default: return status;
            }
        }

        public Status TranslateStatus(Status status)
        {
            switch (status)
            {
                case Status.Avail: return Status.Avail;
                case Status.AvailDesc: goto case Status.Avail;
                case Status.Busy: return Status.Busy;
                case Status.BusyDesc: goto case Status.Busy;
                case Status.DoNotDisturb: return Status.DoNotDisturb;
                case Status.DoNotDisturbDesc: goto case Status.DoNotDisturb;
                case Status.FreeForChat: return Status.FreeForChat;
                case Status.FreeForChatDesc: goto case Status.FreeForChat;
                case Status.Invis: return Status.Invis;
                case Status.InvisDesc: goto case Status.Invis;
                case Status.NotAvail: return Status.NotAvail;
                case Status.NotAvailDesc: goto case Status.NotAvail;
                default: return status;
            }
        }

        public void SetStatus(Status status, string description = null)
        {
            GGNewStatus stat = new GGNewStatus();

            stat.Type = (int)Packets.NewStatus;
            stat.Flags = 0;
            stat.Status = (int)status;
            stat.Description = description;
            stat.DescriptionSize = description == null ? 0 : description.Length;

            stat.Length = 12 + stat.DescriptionSize;

            this.Status = status;
            this.description = description;

            factory.SendPacket(stat);
        }

        public void Disconnect()
        {
            listenerThread.Abort();
            SetStatus(Status.NotAvail);
            socket.Close();
        }

        private void requestImage(string name, int recipient)
        {
            ImgRequest img = new ImgRequest();
            img.Flag = (int)MessageAttribs.ImageRequest;
            img.Crc32 = int.Parse(name.Substring(0, 8));
            img.Size = int.Parse(name.Substring(8, 8));

            SendMessage(string.Empty, recipient, MsgClass.Msg, img.GetBytes());
        }

        /// <summary>
        /// Wysyłanie wiadomości w konferencji
        /// </summary>
        /// <param name="message">Wiadomość</param>
        /// <param name="recipients">Odbiorcy</param>
        public void SendMessage(string message, params int[] recipients)
        {
            foreach (int recipient in recipients)
            {
                MsgRecipients mRec = new MsgRecipients();
                mRec.Flag = (int)MessageAttribs.Conference;
                mRec.Count = recipients.Length - 1;
                mRec.Recipients = recipients.Where(t => t != recipient).ToArray();
                SendMessage(message, recipient);
            }
        }

        public void SendMessage(string message, int recipient, MsgClass cls = MsgClass.Chat, byte[] attribs = null)
        {
            GGSendMsg smsg = new GGSendMsg();

            smsg.Type = (int)Packets.SendMsg;

            smsg.Recipient = recipient;
            smsg.Seq = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
            smsg.Cls = (int)cls;
            smsg.HtmlMessage = string.Format("<span style=\"color:#000000; font-family:'MS Shell Dlg 2'; font-size:9pt; \">{0}</span>\0", message);
            smsg.Message = string.Format("{0}\0", message);
            smsg.PlainOffset = 20 + smsg.HtmlMessage.Length;
            smsg.AttribOffset = smsg.PlainOffset + smsg.Message.Length + 3;
            smsg.Attribs = attribs;

            smsg.Length = 20 + smsg.HtmlMessage.Length + smsg.Message.Length + (attribs == null ? 0 : attribs.Length);

            factory.SendPacket(smsg);
        }

        private void SendList()
        {
            if (users.Count == 0)
            {
                factory.SendPacket(new GGHeader(Packets.ListEmpty, 0));
                return;
            }

            GGNotify notify = new GGNotify();
            notify.Type = (int)Packets.NotifyLast;
            notify.Uin = (from t in users select t.Uin).ToArray();
            notify.UsrType = (from t in users select (byte)t.UserType).ToArray();
            notify.Length = 5 * users.Count;

            factory.SendPacket(notify);
        }
    }
}